﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Xml;

namespace IRC.IRepairClouds.DataLayer
{
    public static class ArgsSerializer
    {
        public static string Serialize(object args)
        {
            if (args == null)
                return null;
            XElement root = new XElement("Params");
            if (args != null) {
                var pis = args.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
                foreach (var pi in pis) {
                    AddProperty(root, pi, args);
                }
            }
            return root.ToString();
        }

        private static void AddProperty(XElement root, PropertyInfo pi, object args)
        {
            Type pt = pi.PropertyType;
            object value = pi.GetValue(args, null);
            if (pt.IsGenericType) {
                if (pt.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
                    pt = pt.GetGenericArguments()[0];
            }
            AddProperty(root, pi.Name, pt, value);
        }

        private static void AddProperty(XElement root, string pn, Type pt, object pv)
        {
            string typeName = null;
            string val = null;
            bool isNotNull = pv != null;

            if (pt.Equals(typeof(DateTime))) {
                typeName = "datetime";
                if (isNotNull)
                    val = XmlConvert.ToString((DateTime)pv, XmlDateTimeSerializationMode.RoundtripKind);
            }
            else if (pt.Equals(typeof(TimeSpan))) {
                typeName = "timespan";
                if (isNotNull)
                    val = XmlConvert.ToString((TimeSpan)pv);
            }
            else if (pt.Equals(typeof(String))) {
                typeName = "string";
                if (isNotNull)
                    val = (String)pv;
            }
            else if (pt.Equals(typeof(Int32))) {
                typeName = "int";
                if (isNotNull)
                    val = XmlConvert.ToString((Int32)pv);
            }
            else if (pt.Equals(typeof(Int64))) {
                typeName = "int64";
                if (isNotNull)
                    val = XmlConvert.ToString((Int64)pv);
            }
            else if (pt.Equals(typeof(Int16))) {
                typeName = "int16";
                if (isNotNull)
                    val = XmlConvert.ToString((Int16)pv);
            }
            else if (pt.Equals(typeof(UInt32))) {
                typeName = "uint";
                if (isNotNull)
                    val = XmlConvert.ToString((UInt32)pv);
            }
            else if (pt.Equals(typeof(UInt64))) {
                typeName = "uint64";
                if (isNotNull)
                    val = XmlConvert.ToString((UInt64)pv);
            }
            else if (pt.Equals(typeof(UInt16))) {
                typeName = "uint16";
                if (isNotNull)
                    val = XmlConvert.ToString((UInt16)pv);
            }
            else if (pt.Equals(typeof(Boolean))) {
                typeName = "boolean";
                if (isNotNull)
                    val = XmlConvert.ToString((Boolean)pv);
            }
            else if (pt.Equals(typeof(Byte))) {
                typeName = "byte";
                if (isNotNull)
                    val = XmlConvert.ToString((Byte)pv);
            }
            else if (pt.Equals(typeof(Char))) {
                typeName = "char";
                if (isNotNull)
                    val = XmlConvert.ToString((Char)pv);
            }
            else if (pt.Equals(typeof(Double))) {
                typeName = "double";
                if (isNotNull)
                    val = XmlConvert.ToString((Double)pv);
            }
            else if (pt.Equals(typeof(Single))) {
                typeName = "single";
                if (isNotNull)
                    val = XmlConvert.ToString((Single)pv);
            }
            else if (pt.Equals(typeof(Decimal))) {
                typeName = "decimal";
                if (isNotNull)
                    val = XmlConvert.ToString((Decimal)pv);
            }
            if (typeName != null) {
                XElement el = new XElement(pn, val,
                    new XAttribute("type", typeName));
                if (val == null)
                    el.Add(new XAttribute("isNull", true));
                root.Add(el);
            }
        }

        private static void GetProperty(XElement root, string pn, out object value, out Type pt)
        {
            value = null;
            pt = null;

            if (root == null)
                return;
            var pe = root.Descendants(pn).SingleOrDefault();
            if (pe == null)
                return;

            string val = pe.Value;
            string typeName = pe.Attribute("type").Value;
            bool isNull = false;
            var ain = pe.Attribute("isNull");
            if (ain != null)
                isNull = XmlConvert.ToBoolean(ain.Value);

            if (typeName == "datetime") {
                if (!isNull)
                    value = XmlConvert.ToDateTime(val, XmlDateTimeSerializationMode.RoundtripKind);
                pt = typeof(DateTime);
                return;
            }
            if (typeName == "timespan") {
                if (!isNull)
                    value = XmlConvert.ToTimeSpan(val);
                pt = typeof(TimeSpan);
                return;
            }
            if (typeName == "string") {
                pt = typeof(String);
                if (!isNull)
                    value = val;
                return;
            }
            if (typeName == "int") {
                pt = typeof(Int32);
                if (!isNull)
                    value = XmlConvert.ToInt32(val);
                return;
            }
            if (typeName == "int64") {
                pt = typeof(Int64);
                if (!isNull)
                    value = XmlConvert.ToInt64(val);
                return;
            }
            if (typeName == "int16") {
                pt = typeof(Int16);
                if (!isNull)
                    value = XmlConvert.ToInt16(val);
                return;
            }
            if (typeName == "uint") {
                pt = typeof(UInt32);
                if (!isNull)
                    value = XmlConvert.ToUInt32(val);
                return;
            }
            if (typeName == "uint64") {
                pt = typeof(UInt64);
                if (!isNull)
                    value = XmlConvert.ToUInt64(val);
                return;
            }
            if (typeName == "uint16") {
                pt = typeof(UInt16);
                if (!isNull)
                    value = XmlConvert.ToUInt16(val);
                return;
            }
            if (typeName == "boolean") {
                pt = typeof(Boolean);
                if (!isNull)
                    value = XmlConvert.ToBoolean(val);
                return;
            }
            if (typeName == "byte") {
                pt = typeof(Byte);
                if (!isNull)
                    value = XmlConvert.ToByte(val);
                return;
            }
            if (typeName == "char") {
                pt = typeof(Char);
                if (!isNull)
                    value = XmlConvert.ToChar(val);
                return;
            }
            if (typeName == "double") {
                pt = typeof(Double);
                if (!isNull)
                    value = XmlConvert.ToDouble(val);
                return;
            }
            if (typeName == "single") {
                pt = typeof(Single);
                if (!isNull)
                    value = XmlConvert.ToSingle(val);
                return;
            }
            if (typeName == "decimal") {
                pt = typeof(Decimal);
                if (!isNull)
                    value = XmlConvert.ToDecimal(val);
                return;
            }
            return;
        }

        public static string Format(string format, string xml)
        {
            if (format == null)
                return null;
            Formatter.NuggetEvaluator ne = new Formatter.NuggetEvaluator(xml);
            return Regex.Replace(format, @"\|\|(?'nugget'.+?)\|\|", ne.Match, RegexOptions.Singleline);
        }

        private static class Formatter
        {
            public class NuggetEvaluator
            {
                const string NA = "N/A";
                XElement _args;

                public NuggetEvaluator(string xml)
                {
                    if (xml != null)
                        _args = XElement.Parse(xml);
                }

                public string Match(Match match)
                {
                    var gNugget = match.Groups["nugget"];
                    object res = null;
                    if (gNugget.Success) {
                        string nugget = gNugget.Value;
                        res = Formatter.Format(nugget, _args);
                    }
                    if (res != null)
                        return res.ToString();
                    return NA;
                }
            }

            private class ParseInfo
            {
                public string Name { get; set; }
                public List<string> Args { get; set; }

                public ParseInfo()
                {
                    Args = new List<string>();
                }
            }

            private class NuggetHolder
            {
                public string Nugget { get; private set; }
                public int Position { get; private set; }

                public NuggetHolder(string nugget)
                {
                    if (nugget == null)
                        throw new ArgumentNullException("nugget");
                    Nugget = nugget;
                    Position = -1;
                }

                public char? GetNext()
                {
                    if ((Position + 1) < Nugget.Length) {
                        Position++;
                        return Nugget[Position];
                    }
                    return null;
                }

                public void MoveNext()
                {
                    if ((Position + 1) < Nugget.Length)
                        Position++;
                    else
                        throw new FormatException();
                }

                public char? PeekNext()
                {
                    if ((Position + 1) < Nugget.Length)
                        return Nugget[Position + 1];
                    return null;
                }

                public char? PeekNextNotEmpty()
                {
                    for (int i = Position + 1; i < Nugget.Length; i++) {
                        char c = Nugget[i];
                        if (c != ' ')
                            return c;
                    }
                    return null;
                }
            }

            private static Dictionary<string, IFormatter> _formatters = new Dictionary<string, IFormatter>();

            private static IFormatter GetFormatter(string name)
            {
                IFormatter res;
                if (_formatters.TryGetValue(name, out res))
                    return res;
                switch (name) {
                    case "Date": res = new Formatter_Date(); break;
                    case "DateTime": res = new Formatter_DateTime(); break;
                    case "DateOnly": res = new Formatter_DateOnly(); break;
                }
                if (res == null)
                    throw new ArgumentOutOfRangeException("Formatter [" + name + "] was not found!");
                _formatters[name] = res;
                return res;
            }

            private interface IFormatter
            {
                object Format(object source, List<string> args);
            }

            private class Formatter_Date : IFormatter
            {
                public object Format(object source, List<string> args)
                {
                    if (source != null) {
                        if (source is DateTime) {
                            return ((DateTime)source).ToClientTime().CDateToStrDay();
                        }
                        else
                            throw new FormatException("Cannot convert source to DateTime");
                    }
                    return source;
                }
            }

            private class Formatter_DateTime : IFormatter
            {
                public object Format(object source, List<string> args)
                {
                    if (source != null) {
                        if (source is DateTime) {
                            return ((DateTime)source).ToClientTime().CDateToStr();
                        }
                        else
                            throw new FormatException("Cannot convert source to DateTime");
                    }
                    return source;
                }
            }

            private class Formatter_DateOnly : IFormatter
            {
                public object Format(object source, List<string> args)
                {
                    if (source != null) {
                        if (source is DateTime) {
                            return ((DateTime)source).ToClientDate().CDateToStrDay();
                        }
                        else
                            throw new FormatException("Cannot convert source to DateTime");
                    }
                    return source;
                }
            }

            private static List<ParseInfo> Parse(string nugget)
            {
                List<ParseInfo> res = new List<ParseInfo>();

                StringBuilder sb = new StringBuilder();
                bool isName = false;
                bool isArg = false;
                bool isArgs = false;
                sb.Remove(0, sb.Length);

                ParseInfo pi = null;
                NuggetHolder nh = new NuggetHolder(nugget);
                char? c;
                while ((c = nh.GetNext()) != null) {
                    char cv = c.Value;
                    if (isName) {
                        switch (cv) {
                            case ' ': {
                                    var nne = nh.PeekNextNotEmpty();
                                    if (nne != null && nne.Value != '(' && nne.Value != '>')
                                        throw new FormatException();
                                }
                                break;
                            case '(': {
                                    pi = new ParseInfo();
                                    res.Add(pi);
                                    pi.Name = sb.ToString();
                                    sb.Remove(0, sb.Length);
                                    isName = false;
                                    isArgs = true;
                                }
                                break;
                            case '>': {
                                    var nc = nh.PeekNext();
                                    if (nc != null && nc == '>') {
                                        nh.MoveNext();
                                        pi = new ParseInfo();
                                        res.Add(pi);
                                        pi.Name = sb.ToString();
                                        sb.Remove(0, sb.Length);
                                        isName = false;
                                    }
                                    else
                                        throw new FormatException();
                                }
                                break;
                            default: {
                                    sb.Append(cv);
                                }
                                break;
                        }
                    }
                    else if (isArgs) {
                        if (isArg) {
                            switch (cv) {
                                case '\'': {
                                        var nc = nh.PeekNext();
                                        if (nc != null && nc == '\'') {
                                            sb.Append(cv);
                                            nh.MoveNext();
                                        }
                                        else {
                                            pi.Args.Add(sb.ToString());
                                            sb.Remove(0, sb.Length);
                                            isArg = false;
                                        }
                                    }
                                    break;
                                default: {
                                        sb.Append(cv);
                                    }
                                    break;
                            }
                        }
                        else {
                            switch (cv) {
                                case ' ':
                                    break;
                                case ',':
                                    break;
                                case '\'':
                                    isArg = true;
                                    break;
                                case ')':
                                    isArgs = false;
                                    break;
                                default:
                                    throw new FormatException();
                            }
                        }
                    }
                    else {
                        switch (cv) {
                            case ' ':
                                break;
                            default: {
                                    sb.Append(cv);
                                    isName = true;
                                }
                                break;
                        }
                    }
                }
                if (isName) {
                    pi = new ParseInfo();
                    res.Add(pi);
                    pi.Name = sb.ToString();
                    sb.Remove(0, sb.Length);
                    isName = false;
                }
                else {
                    if (isArgs)
                        throw new FormatException();
                }
                return res;
            }

            public static object Format(string nugget, XElement args)
            {
                var pis = Parse(nugget);
                if (pis.Count > 0) {
                    object val;
                    Type pt;
                    ArgsSerializer.GetProperty(args, pis[0].Name, out val, out pt);
                    for (int i = 1; i < pis.Count; i++) {
                        var pi = pis[i];
                        var f = Formatter.GetFormatter(pi.Name);
                        val = f.Format(val, pi.Args);
                    }
                    return val;
                }
                return null;
            }
        }
    }
}
